#include "MultiDonwloader.h"
#include <thread>
#include <cstdlib>
#include "curl/curl.h"

USING_NS_CC;

namespace elloop {

	MultiDownloader * MultiDownloader::_instance = nullptr;

	MultiDownloader::MultiDownloader() : _isCurlGlobalInited(false) {}

	MultiDownloader::~MultiDownloader() {
		if (_isCurlGlobalInited) {
			curl_global_cleanup();
		}
	}

	void MultiDownloader::startTasks() {
		_taskListMutex.lock();

		if (!_isCurlGlobalInited) {
			// must call this before using curl in threads.
			_isCurlGlobalInited = curl_global_init(CURL_GLOBAL_ALL);
		}

		if (!_isCurlGlobalInited) {
			CCLOG("curl_global_init fail! in MultiDownloader::startTasks()\n");
			return;
		}

		auto iter = _taskList.begin();
		while (iter != _taskList.end()) {
			TaskPtr task = *iter;

			// start a new thread to download the task.
			std::thread downloadThread(&MultiDownloader::downloadThreadFunc, this, task);
			downloadThread.detach();

			++iter;
		}
		_taskList.clear();

		_taskListMutex.unlock();
	}

	void MultiDownloader::pushTask(const std::string & url, const std::string & saveFileName) {
		if (!url.empty()) {
			TaskPtr pTask(new DownloadTask(url, saveFileName));
			_taskListMutex.lock();
			_taskList.push_back(pTask);
			_taskListMutex.unlock();
		}
	}

	void MultiDownloader::pushTask(TaskPtr pTask) {
		if (pTask != nullptr) {
			_taskListMutex.lock();
			_taskList.push_back(pTask);
			_taskListMutex.unlock();
		}
	}

	size_t writeFunction(char *content, size_t size, size_t nmemb, void *userdata) {
		DownloadTask* pTask = (DownloadTask*)userdata;
		if (pTask) {
			return pTask->writeFunction(content, size, nmemb, userdata);
		}
		CCLOG("illegal clientPtr in curl writeFunction\n");
		return 0;
	}

	int progressFunction(void * clientPtr, double downloadTotal, double downloaded, double upTotal, double upLoaded) {
		DownloadTask * pTask = (DownloadTask*)clientPtr;
		if (nullptr == pTask) {
			CCLOG("illegal clientPtr in curl progressFunction\n");
			return 0;
		}
		else {
			return pTask->progressFunction(clientPtr, downloadTotal, downloaded, upTotal, upLoaded);
		}
	}

	void MultiDownloader::downloadThreadFunc(TaskPtr pTask) {

		if (nullptr == pTask) {
			CCLOG("[MultiDownloader error]: TaskPtr cannot be null!\n");
			return;
		}

		CURL * curl = curl_easy_init();
		if (nullptr == curl) {
			CCLOG("fail to init curl!!!\n");
			return;
		}

		curl_easy_setopt(curl, CURLOPT_URL, pTask->url().c_str());
		//curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, std::bind(&DownloadTask::writeFunction, std::placeholders::_1, pTask.get()));
		curl_easy_setopt(curl, CURLOPT_WRITEDATA, pTask.get());
		curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeFunction);
		curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
		curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30 * 60);
		curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0);
		//curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, std::bind(&DownloadTask::progressFunction, std::placeholders::_1, pTask.get()));
		curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, pTask.get());
		curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, progressFunction);

		time_t begin = clock();

		CURLcode resultCode = curl_easy_perform(curl);

		time_t end = clock();

		curl_easy_cleanup(curl);

		CCLOG("%s \n Done, time consuming: %.2f\n", pTask->url().c_str(), (double)(end - begin) / CLOCKS_PER_SEC);

		if (resultCode == CURLE_OK) {
			pTask->onFileDownloaded();
			_finishedListMutex.lock();
			_finishedList.push_back(pTask);
			_finishedListMutex.unlock();
		}
		else {
			_failedListMutex.lock();
			_failedList.push_back(pTask);
			_failedListMutex.unlock();
			CCLOG("curl error: %s\n", curl_easy_strerror(resultCode));
		}
	}

	/************************************************************************/
	/*                   implementation of DownloadTask 
	/************************************************************************/
	DownloadTask::DownloadTask(const std::string & url, const std::string& tag /*= ""*/) :
		_url(url),
		_tag(tag),
		_data(nullptr),
		_size(0),
		_downloadedSize(0),
		_totalSize(0),
		_progressFunction(nullptr),
		_writeFunction(nullptr),
		_saveDataFunction(nullptr),
		_downloadProgressCallback(nullptr) 
	{}

	DownloadTask::~DownloadTask() {
		if (_data != nullptr) {
			free(_data);
			_data = nullptr;
		}
	}

	size_t DownloadTask::writeFunction(char *content, size_t size, size_t nmemb, void *userdata) {

		if (_writeFunction != nullptr) {
			return _writeFunction(content, size, nmemb, userdata);
		}

		size_t realSize = size * nmemb;
		_data = (unsigned char *)realloc(_data, _size + realSize);
		if (_data == nullptr) {
			CCLOG("fail to write data, memory not enough\n");
			return 0;
		}
		memcpy(_data + _size, content, realSize);
		_size += realSize;

		return realSize;
	}

	int DownloadTask::progressFunction(void * clientPtr, double downloadTotal, double downloaded, double upTotal, double upLoaded) {
		_totalSize = downloadTotal;
		_downloadedSize = downloaded;

		if (_progressFunction != nullptr) {
			return _progressFunction(clientPtr, downloadTotal, downloaded, upTotal, upLoaded);
		}

		if (abs(downloadTotal - 0) > 0.001f) {
			CCLOG("downloading [%s] : %.2f/%.2f -- %.2f%%\n", _tag.c_str(), downloaded, downloadTotal, downloaded / downloadTotal * 100);
		}

		if (_downloadProgressCallback != nullptr) {
			_downloadProgressCallback(downloadTotal, downloaded);
		}

		return 0;
	}

	void DownloadTask::onFileDownloaded() {
		if (_saveDataFunction != nullptr) {
			return _saveDataFunction(_data, _size);
		}

		if (_tag.empty()) {
			// todo: add a timestap into tag.
			_tag = "noname";
		}

		if (_data) {
			// wirte data into file.
			std::string path = cocos2d::CCFileUtils::sharedFileUtils()->getWritablePath() + "/" + _tag;
			FILE * fp = fopen(path.c_str(), "wb");
			fwrite(_data, 1, _size, fp);
			fclose(fp);
			CCLOG("file %s saved\n", path.c_str());
		}

		if (_data) {
			free(_data);
			_data = nullptr;
		}
	}
}
